package jsr292.weaver.opt;

import java.lang.reflect.Field;
import java.util.HashMap;

import jsr292.weaver.Utils;
import jsr292.weaver.Wrapper;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;


public class OptimizerBoundMap {
  private String ownerClassName;
  private Object[] values;
  private int valueIndex;
  private ClassWriter cv;
  private MethodVisitor staticInitMv;
  
  private static final HashMap<String, Object[]> EXCHANGE_MAP =
          new HashMap<String, Object[]>();
  private static final String THIS_CLASS_NAME =
          OptimizerBoundMap.class.getName().replace('.', '/');
  
  OptimizerBoundMap() {
      // fields are lazy initialized in bound/loadBound
  }
  
  public /*maybenull*/String getOwnerClassName() {
      return ownerClassName;
  }
  
  public boolean isEmpty() {
      return ownerClassName == null;
  }
  
  private void lazyAllocate() {
      ownerClassName = "jsr292/weaver/opt/EphemeralBoundMap"+Optimizer.nextBoundMapId();
      values = new Object[4];
      cv = new ClassWriter(0);
      cv.visit(Opcodes.V1_5, Opcodes.ACC_PUBLIC|Opcodes.ACC_SUPER, ownerClassName, null, "java/lang/Object", null);
      
      MethodVisitor mv = cv.visitMethod(Opcodes.ACC_STATIC, "<clinit>", "()V", null, null);
      mv.visitCode();
      mv.visitLdcInsn(ownerClassName);
      mv.visitMethodInsn(Opcodes.INVOKESTATIC, THIS_CLASS_NAME, "extractValues", "(Ljava/lang/String;)[Ljava/lang/Object;");
      mv.visitVarInsn(Opcodes.ASTORE, 0);  // values is now in slot 0
      staticInitMv = mv;
  }
  
  void bindAndLoad(MethodVisitor mv, Object o, Class<?> declaredClass, Class<?> boundClass) {
      if (o == null) {
	  mv.visitInsn(Opcodes.ACONST_NULL);
	  return;
      }
      
      Class<? extends Object> clazz = o.getClass();
      Wrapper wrapper = Wrapper.getWrapper(clazz);
      if (wrapper != null || clazz == String.class) {
	  mv.visitLdcInsn(o);
	  return;
      }
      if (clazz == Class.class) {
	  mv.visitLdcInsn(Type.getType((Class<?>)o));
	  return;
      }
      
      if (ownerClassName == null)
          lazyAllocate();
      
      ensureCapacity();
      
      int slot = valueIndex++;
      values[slot] = o;
      Type boundType = Type.getType(boundClass);
      
      String fieldName = addConstantEntry(slot, boundType);
      
      String desc = boundType.getDescriptor();
      mv.visitFieldInsn(Opcodes.GETSTATIC, ownerClassName, fieldName, desc);
      if (boundClass != declaredClass && !declaredClass.isAssignableFrom(boundClass)) {
          mv.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(declaredClass));
      }
  }
  
  
  public String bind(Object o, Type boundType) {
      if (ownerClassName == null)
          lazyAllocate();
      
      ensureCapacity();
      int slot = valueIndex++;
      values[slot] = o;
      return addConstantEntry(slot, boundType);
  }
  
  public void loadAndBindStaticFieldBase(MethodVisitor mv, Field field) {
      String boundFieldName = bind(field, Type.getObjectType("java/lang/reflect/Field"));
      
      ensureCapacity();
      int slot = valueIndex++;
      values[slot] = null;
      
      // add field
      String fieldName = "$bound$" + slot;
      
      //XXX if this field is final, it crashs the VM with jdk6 because the result of unsafe.staticFieldBase is not a real object
      FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC /*| Opcodes.ACC_FINAL*/, fieldName, "Ljava/lang/Object;", null, null);  
      fv.visitEnd();
      
      // add init sequence in static init
      MethodVisitor staticInitMv = this.staticInitMv;
      staticInitMv.visitFieldInsn(Opcodes.GETSTATIC, ownerClassName, boundFieldName, "Ljava/lang/reflect/Field;");
      staticInitMv.visitMethodInsn(Opcodes.INVOKESTATIC, "jsr292/weaver/Capabilities", "staticFieldBase", "(Ljava/lang/reflect/Field;)Ljava/lang/Object;");
      staticInitMv.visitFieldInsn(Opcodes.PUTSTATIC, ownerClassName, fieldName, "Ljava/lang/Object;");
      
      mv.visitFieldInsn(Opcodes.GETSTATIC, ownerClassName, fieldName, "Ljava/lang/Object;");
      
      return ;
  }
  
  private void ensureCapacity() {
      int length = values.length;
      if (valueIndex == length) {
          Object[] array = new Object[length * 2];
          System.arraycopy(values, 0, array, 0, length);
          values = array;
      }
  }
  
  private String addConstantEntry(int slot, Type boundType) {
      // add field
      String fieldName = "$bound$" + slot;
      String desc = boundType.getDescriptor();
      FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL, fieldName, desc, null, null);  
      fv.visitEnd();
      
      // add init sequence in static init
      MethodVisitor staticInitMv = this.staticInitMv;
      staticInitMv.visitVarInsn(Opcodes.ALOAD, 0);
      Utils.ldcInteger(staticInitMv, slot);
      staticInitMv.visitInsn(Opcodes.AALOAD);
      String internalName = boundType.getInternalName();
      if (!internalName.equals("java/lang/Object")) {
          staticInitMv.visitTypeInsn(Opcodes.CHECKCAST, internalName);
      }
      staticInitMv.visitFieldInsn(Opcodes.PUTSTATIC, ownerClassName, fieldName, desc);
      return fieldName;
  }
  
  /** this method must be called only once by object.
   */
  public byte[] toByteArray() {
      synchronized(EXCHANGE_MAP) {
        EXCHANGE_MAP.put(ownerClassName, values);
      }
      
      staticInitMv.visitInsn(Opcodes.RETURN);
      staticInitMv.visitMaxs(2, 1);
      staticInitMv.visitEnd();
      cv.visitEnd();
      return cv.toByteArray();
  }
  
  // entry point called directly from bytecode
  public static Object[] extractValues(String ownerClassName) {
      synchronized(EXCHANGE_MAP) {
          return EXCHANGE_MAP.remove(ownerClassName);
      }
  }
}
